#include <string>

#include "google/protobuf/compiler/code_generator.h"
#include "google/protobuf/compiler/plugin.h"
#include "google/protobuf/compiler/plugin.pb.h"
#include "google/protobuf/descriptor.h"

#include "ytlib/string/string_util.hpp"

namespace ytlib {
namespace ytrpc {

class UnifexRpcCodeGenerator final : public google::protobuf::compiler::CodeGenerator {
 public:
  UnifexRpcCodeGenerator() = default;
  virtual ~UnifexRpcCodeGenerator() = default;

  constexpr static std::string_view t_ccfile = R"str(/**
 * @file {{file_name}}.unifex_rpc.pb.cc
 * @brief This file was generated by protoc-gen-unifex_rpc which is a self-defined pb compiler plugin, do not edit it!!!
 */
)str";

  constexpr static std::string_view t_hfile_one_service_register_func = R"str()str";

  constexpr static std::string_view t_hfile_one_service_func = R"str(
  virtual auto {{rpc_func_name}}(const std::shared_ptr<const ytlib::ytrpc::UnifexRpcContext>& ctx_ptr, const {{rpc_req_name}}& req)
      -> unifex::task<std::tuple<ytlib::ytrpc::UnifexRpcStatus, {{rpc_rsp_name}}>> {
    co_return {ytlib::ytrpc::UnifexRpcStatus(ytlib::ytrpc::UnifexRpcStatus::Code::NOT_IMPLEMENTED), {{rpc_rsp_name}}()};
  })str";

  constexpr static std::string_view t_hfile_one_service_class = R"str(
class {{service_name}} : public ytlib::ytrpc::UnifexRpcService {
 public:
  {{service_name}}() {
{{service_register_func}}
  }

  virtual ~{{service_name}}() = default;
{{service_func}}
};)str";

  constexpr static std::string_view t_hfile_one_service_proxy_func = R"str(
  auto {{rpc_func_name}}(const std::shared_ptr<const ytlib::ytrpc::UnifexRpcContext>& ctx_ptr, const {{rpc_req_name}}& req)
      -> ytlib::ytrpc::UnifexRpcSender<{{rpc_req_name}}, {{rpc_rsp_name}}> {
    const static std::string func_name("/{{package_name}}.{{service_name}}/{{rpc_func_name}}");
    return Invoke<{{rpc_req_name}}, {{rpc_rsp_name}}>(func_name, ctx_ptr, req);
  })str";

  constexpr static std::string_view t_hfile_one_service_proxy_class = R"str(
class {{service_name}}Proxy final : private ytlib::ytrpc::UnifexRpcServiceProxy {
 public:
  explicit {{service_name}}Proxy(const std::shared_ptr<ytlib::ytrpc::UnifexRpcClient>& client_ptr) : ytlib::ytrpc::UnifexRpcServiceProxy(client_ptr) {}

  virtual ~{{service_name}}Proxy() = default;
{{service_proxy_func}}
};)str";

  constexpr static std::string_view t_hfile = R"str(/**
 * @file {{file_name}}.unifex_rpc.pb.h
 * @brief This file was generated by protoc-gen-unifex_rpc which is a self-defined pb compiler plugin, do not edit it!!!
 */
#pragma once

#include "ytlib/ytrpc/unifex_rpc/unifex_rpc_client.hpp"
#include "ytlib/ytrpc/unifex_rpc/unifex_rpc_server.hpp"

#include "{{file_name}}.pb.h"

namespace {{namespace_name}} {
{{service_class}}
{{service_proxy_class}}
}  // namespace {{namespace_name}}
)str";

  static std::string ProtoFileBaseName(const std::string& full_name) {
    return full_name.substr(0, full_name.rfind("."));
  }

  static std::string GenNamespaceStr(const std::string& ns) {
    std::string result = ns;
    return ytlib::ReplaceString(result, ".", "::");
  }

  static void WriteToFile(google::protobuf::compiler::GeneratorContext* context, const std::string& file_name, const std::string& file_context) {
    std::unique_ptr<google::protobuf::io::ZeroCopyOutputStream> output(context->Open(file_name));
    google::protobuf::io::CodedOutputStream coded_out(output.get());
    coded_out.WriteRaw(file_context.data(), file_context.size());
  }

  bool Generate(const google::protobuf::FileDescriptor* file, const std::string& parameter,
                google::protobuf::compiler::GeneratorContext* context, std::string* error) const override {
    const std::string& file_name = ProtoFileBaseName(file->name());
    const std::string& package_name = file->package();
    const std::string& namespace_name = GenNamespaceStr(file->package());

    // ccfile
    std::string ccfile = std::string(t_ccfile);
    ytlib::ReplaceString(ccfile, "{{file_name}}", file_name);
    WriteToFile(context, file_name + ".unifex_rpc.pb.cc", ccfile);

    // hfile
    std::string hfile_service_class;
    std::string hfile_service_proxy_class;

    for (int ii = 0; ii < file->service_count(); ++ii) {
      auto service = file->service(ii);

      if (ii != 0) {
        hfile_service_class += "\n";
        hfile_service_proxy_class += "\n";
      }

      const std::string& service_name = service->name();

      std::string hfile_service_register_func;
      std::string hfile_service_func;

      std::string hfile_service_proxy_func;

      for (int jj = 0; jj < service->method_count(); ++jj) {
        auto method = service->method(jj);

        if (jj != 0) {
          hfile_service_register_func += "\n";
          hfile_service_func += "\n";
          hfile_service_proxy_func += "\n";
        }

        const std::string& rpc_func_name = method->name();
        const std::string& rpc_req_name = GenNamespaceStr(method->input_type()->full_name());
        const std::string& rpc_rsp_name = GenNamespaceStr(method->output_type()->full_name());

        std::string hfile_one_service_register_func = std::string(t_hfile_one_service_register_func);
        ytlib::ReplaceString(hfile_one_service_register_func, "{{rpc_req_name}}", rpc_req_name);
        ytlib::ReplaceString(hfile_one_service_register_func, "{{rpc_rsp_name}}", rpc_rsp_name);
        ytlib::ReplaceString(hfile_one_service_register_func, "{{package_name}}", package_name);
        ytlib::ReplaceString(hfile_one_service_register_func, "{{service_name}}", service_name);
        ytlib::ReplaceString(hfile_one_service_register_func, "{{rpc_func_name}}", rpc_func_name);

        hfile_service_register_func += hfile_one_service_register_func;

        std::string hfile_one_service_func = std::string(t_hfile_one_service_func);
        ytlib::ReplaceString(hfile_one_service_func, "{{rpc_req_name}}", rpc_req_name);
        ytlib::ReplaceString(hfile_one_service_func, "{{rpc_rsp_name}}", rpc_rsp_name);
        ytlib::ReplaceString(hfile_one_service_func, "{{rpc_func_name}}", rpc_func_name);

        hfile_service_func += hfile_one_service_func;

        std::string hfile_one_service_proxy_func = std::string(t_hfile_one_service_proxy_func);
        ytlib::ReplaceString(hfile_one_service_proxy_func, "{{rpc_req_name}}", rpc_req_name);
        ytlib::ReplaceString(hfile_one_service_proxy_func, "{{rpc_rsp_name}}", rpc_rsp_name);
        ytlib::ReplaceString(hfile_one_service_proxy_func, "{{package_name}}", package_name);
        ytlib::ReplaceString(hfile_one_service_proxy_func, "{{service_name}}", service_name);
        ytlib::ReplaceString(hfile_one_service_proxy_func, "{{rpc_func_name}}", rpc_func_name);

        hfile_service_proxy_func += hfile_one_service_proxy_func;
      }

      std::string hfile_one_service_class = std::string(t_hfile_one_service_class);
      ytlib::ReplaceString(hfile_one_service_class, "{{service_name}}", service_name);
      ytlib::ReplaceString(hfile_one_service_class, "{{service_register_func}}", hfile_service_register_func);
      ytlib::ReplaceString(hfile_one_service_class, "{{service_func}}", hfile_service_func);

      hfile_service_class += hfile_one_service_class;

      std::string hfile_one_service_proxy_class = std::string(t_hfile_one_service_proxy_class);
      ytlib::ReplaceString(hfile_one_service_proxy_class, "{{service_name}}", service_name);
      ytlib::ReplaceString(hfile_one_service_proxy_class, "{{service_proxy_func}}", hfile_service_proxy_func);

      hfile_service_proxy_class += hfile_one_service_proxy_class;
    }

    std::string hfile = std::string(t_hfile);
    ytlib::ReplaceString(hfile, "{{file_name}}", file_name);
    ytlib::ReplaceString(hfile, "{{namespace_name}}", namespace_name);
    ytlib::ReplaceString(hfile, "{{service_class}}", hfile_service_class);
    ytlib::ReplaceString(hfile, "{{service_proxy_class}}", hfile_service_proxy_class);

    WriteToFile(context, file_name + ".unifex_rpc.pb.h", hfile);

    return true;
  }
};

}  // namespace ytrpc
}  // namespace ytlib

int main(int argc, char* argv[]) {
  ytlib::ytrpc::UnifexRpcCodeGenerator generator;
  return google::protobuf::compiler::PluginMain(argc, argv, &generator);
}
